@codeyam-editor/codeyam-editor 0.1.8 → 0.1.10
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/npm/cloud.js +377 -21
- package/npm/editor-dev.js +24 -2
- package/npm/editor.js +15 -5
- package/npm/fleet-add-failure.js +355 -21
- package/npm/fleet-auth-account.js +61 -0
- package/npm/fleet-auth.js +69 -1
- package/npm/fleet-branch.js +124 -1
- package/npm/fleet-build-data.js +12 -1
- package/npm/fleet-cleanup.js +84 -3
- package/npm/fleet-datastore-health.js +107 -0
- package/npm/fleet-public-url.js +81 -12
- package/npm/fleet-reconcile.js +46 -11
- package/npm/fleet-revision.js +108 -0
- package/npm/fleet-run-setup.js +66 -0
- package/npm/fleet-session-activity.js +162 -0
- package/npm/fleet-vm-readiness.js +57 -16
- package/npm/group-bounded-spawn.js +111 -0
- package/npm/launch-config.js +216 -23
- package/npm/orphan-ssh-reaper.js +81 -0
- package/npm/scenario-check.js +697 -34
- package/npm/scenario-interactivity.js +477 -23
- package/npm/scenario-issues.js +61 -0
- package/npm/scenario-playwright.js +174 -20
- package/npm/utils.js +262 -22
- package/package.json +5 -5
- package/ui/dist/assets/{ContainerInspector-DSVZvEkB.js → ContainerInspector-DiN91foX.js} +1 -1
- package/ui/dist/assets/EditorShell-BEIdHnHd.css +1 -0
- package/ui/dist/assets/EditorShell-CPV5vhjo.js +330 -0
- package/ui/dist/assets/ScenarioDataPanel-DHAHyraq.js +18 -0
- package/ui/dist/assets/{ScenarioDataPanelFullPage-Cyk6efiI.js → ScenarioDataPanelFullPage-CHIWip3n.js} +1 -1
- package/ui/dist/assets/ScenarioRenderer-D9a91iwS.js +21 -0
- package/ui/dist/assets/index--cE0hUJw.css +1 -0
- package/ui/dist/assets/{index-BPLKgwon.js → index-sW8wcg7v.js} +9 -9
- package/ui/dist/index.html +2 -2
- package/ui/dist/preview-subdir-demo/index.html +62 -0
- package/ui/dist/preview-subdir-demo/sibling.html +48 -0
- package/ui/dist/assets/EditorShell-DnXdHrar.js +0 -256
- package/ui/dist/assets/EditorShell-FvbXyqqD.css +0 -1
- package/ui/dist/assets/ScenarioDataPanel-6qJE-0lA.js +0 -15
- package/ui/dist/assets/ScenarioRenderer-Uyt54vHI.js +0 -21
- package/ui/dist/assets/index-DKpgJLo3.css +0 -1
package/npm/cloud.js
CHANGED
|
@@ -23,6 +23,11 @@ const {
|
|
|
23
23
|
killChildProcess,
|
|
24
24
|
} = require("./utils");
|
|
25
25
|
const { open } = require("./open");
|
|
26
|
+
// Bounded gcloud/ssh spawns run through spawnGroupBoundedSync so a timeout takes
|
|
27
|
+
// gcloud's ssh descendant down with it instead of orphaning it to PID 1; the
|
|
28
|
+
// reaper sweeps sessions already leaked before that existed.
|
|
29
|
+
const { spawnGroupBoundedSync } = require("./group-bounded-spawn");
|
|
30
|
+
const { reapOrphanedFleetSsh } = require("./orphan-ssh-reaper");
|
|
26
31
|
// `FORWARD_ENV_KEYS_ENV` is the env-var name (`CY_FORWARD_ENV_KEYS`) the
|
|
27
32
|
// fleet dashboard sets on `cloud:up`'s child to name the subset of
|
|
28
33
|
// `process.env` keys that came from the operator-supplied launch form.
|
|
@@ -514,6 +519,28 @@ function previewSameOriginSafeFromStack(stackJson) {
|
|
|
514
519
|
return typeof flag === "boolean" ? flag : true;
|
|
515
520
|
}
|
|
516
521
|
|
|
522
|
+
/** Resolve the sameOriginSafe flag a seed should ACT on, given the static
|
|
523
|
+
* `stack.json` reading and the running editor's own `/api/config` verdict.
|
|
524
|
+
*
|
|
525
|
+
* Precedence: a BOOLEAN effective verdict wins; anything else (`null`,
|
|
526
|
+
* `undefined`, a non-boolean) defers to the static flag. The type check is the
|
|
527
|
+
* whole rule — `null` means "no server was up to ask", which is the
|
|
528
|
+
* pre-container seed and must leave the static read in charge, NOT "the server
|
|
529
|
+
* said same-origin is fine". Collapsing the two would put the static flag back
|
|
530
|
+
* in control on exactly the path that needs the override.
|
|
531
|
+
*
|
|
532
|
+
* Extracted from `seedPreviewOrigin` because that function's inputs arrive over
|
|
533
|
+
* gcloud SSH, so this rule was only reachable through heavy mocking — while
|
|
534
|
+
* being precisely where the defect lived. `stack.json` cannot express the state
|
|
535
|
+
* that matters here: a project escalates off the same-origin subpath at RUNTIME
|
|
536
|
+
* after a hydration failure, and the adaptive-default stacks that happens to
|
|
537
|
+
* (Next.js) deliberately omit the key. So a seeder that re-derives the flag
|
|
538
|
+
* from the file writes back the same-origin sentinel that just failed, and the
|
|
539
|
+
* project sits at `previewDedicatedOriginUnavailable: true` forever. Pure. */
|
|
540
|
+
function resolveEffectiveSameOriginSafe(staticFlag, effectiveVerdict) {
|
|
541
|
+
return typeof effectiveVerdict === "boolean" ? effectiveVerdict : staticFlag;
|
|
542
|
+
}
|
|
543
|
+
|
|
517
544
|
// Matches an absolute cloudflared quick-tunnel origin
|
|
518
545
|
// (`https://<sub>.trycloudflare.com`, optionally with a port/path/query). These
|
|
519
546
|
// URLs ROTATE on every tunnel respawn, which is exactly why a frozen one rots.
|
|
@@ -958,6 +985,19 @@ function buildGcloudArgs(verb, cfg = {}, extras = {}) {
|
|
|
958
985
|
return ["compute", "instances", "delete", instanceName, "--project", project, "--zone", zone, "--quiet"];
|
|
959
986
|
case "instances-describe":
|
|
960
987
|
return ["compute", "instances", "describe", instanceName, "--project", project, "--zone", zone, "--format=json"];
|
|
988
|
+
// Outstanding (not-yet-DONE) zone operations targeting this instance. Used to
|
|
989
|
+
// tell a normal transient PROVISIONING from a wedged insert: GCE can accept an
|
|
990
|
+
// insert and then never finish it, leaving an operation parked at RUNNING/0%
|
|
991
|
+
// that holds the instance name forever. The status filter is what makes this
|
|
992
|
+
// cheap and unambiguous — a DONE operation is history and says nothing about
|
|
993
|
+
// whether the slot is currently blocked.
|
|
994
|
+
case "operations-list-pending":
|
|
995
|
+
return [
|
|
996
|
+
"compute", "operations", "list",
|
|
997
|
+
"--project", project,
|
|
998
|
+
"--filter", `zone:(${zone}) AND targetLink~${instanceName}$ AND status!=DONE`,
|
|
999
|
+
"--format=json",
|
|
1000
|
+
];
|
|
961
1001
|
case "instances-list-tagged":
|
|
962
1002
|
return [
|
|
963
1003
|
"compute", "instances", "list",
|
|
@@ -1054,8 +1094,45 @@ function buildGcloudArgs(verb, cfg = {}, extras = {}) {
|
|
|
1054
1094
|
// reconcile spawnSync sites (status probe, per-BES timeout converge) so all three
|
|
1055
1095
|
// reference one value. 2 min is generous for any single describe/update/SSH probe
|
|
1056
1096
|
// yet comfortably under the 600000 ms outer reconcile budget.
|
|
1097
|
+
//
|
|
1098
|
+
// All three spawn through spawnGroupBoundedSync, because a bare spawnSync timeout
|
|
1099
|
+
// signals only the direct child: gcloud died and its `ssh` survived, re-parented
|
|
1100
|
+
// to PID 1 — the budget bounded the wrapper, never the descendant.
|
|
1101
|
+
//
|
|
1102
|
+
// It is a PROBE budget, and provisioning is exempt — see
|
|
1103
|
+
// GCLOUD_PROVISION_TIMEOUT_MS below. Do not re-collapse the two: raising this
|
|
1104
|
+
// constant to accommodate a slow create would un-bound the reconcile sweep,
|
|
1105
|
+
// which is the exact orphan-cascade this value exists to stop.
|
|
1057
1106
|
const GCLOUD_CALL_TIMEOUT_MS = 120000;
|
|
1058
1107
|
|
|
1108
|
+
// `instances create` / `instances start` are NOT probes: each blocks until GCE's
|
|
1109
|
+
// insert (or start) operation reaches a terminal state, which routinely outruns
|
|
1110
|
+
// the probe budget under zonal contention. Inheriting GCLOUD_CALL_TIMEOUT_MS
|
|
1111
|
+
// SIGTERMd gcloud mid-insert; `spawnSync` then returned `status: null`, which
|
|
1112
|
+
// `!== 0`, so a local kill was indistinguishable from a remote rejection and the
|
|
1113
|
+
// caller threw a bare `Failed to create instance <name>` with no cause in it.
|
|
1114
|
+
// That mattered downstream: every arm of classifyAddFailure keys on a transcript
|
|
1115
|
+
// signature, so a causeless log fell through to the optimistic `verifying`
|
|
1116
|
+
// fallback and told the operator to WAIT on a VM GCE had already wedged
|
|
1117
|
+
// (observed 2026-09-04 on VM-8: insert 14:29:50Z, add errored 14:32:26Z — the
|
|
1118
|
+
// 120s budget plus teardown).
|
|
1119
|
+
//
|
|
1120
|
+
// Deliberately above the 600000 ms outer reconcile budget, on the same reasoning
|
|
1121
|
+
// as PROJECT_BOOTSTRAP_TIMEOUT_MS: ensureInstance is only ever reached from the
|
|
1122
|
+
// add path, never from the reconcile sweep, so the sweep-relative ceiling that
|
|
1123
|
+
// governs GCLOUD_CALL_TIMEOUT_MS does not apply. A create still running at 10
|
|
1124
|
+
// minutes is wedged, not slow.
|
|
1125
|
+
const GCLOUD_PROVISION_TIMEOUT_MS = 10 * 60 * 1000;
|
|
1126
|
+
|
|
1127
|
+
/** The per-call spawn budget a given wrapper verb should run under. Provisioning
|
|
1128
|
+
* verbs get GCLOUD_PROVISION_TIMEOUT_MS; everything else keeps the probe budget.
|
|
1129
|
+
* Pure — the mapping is asserted directly, without spawning a process. */
|
|
1130
|
+
function gcloudCallTimeoutMs(verb) {
|
|
1131
|
+
return verb === "instances-create" || verb === "instances-start"
|
|
1132
|
+
? GCLOUD_PROVISION_TIMEOUT_MS
|
|
1133
|
+
: GCLOUD_CALL_TIMEOUT_MS;
|
|
1134
|
+
}
|
|
1135
|
+
|
|
1059
1136
|
// The project bootstrap is NOT a probe: it runs `git clone` + `npm install` in
|
|
1060
1137
|
// the repo root and every `apps[].dir`, each package's `postinstall`, then
|
|
1061
1138
|
// `npm run setup` — minutes of remote work on a heavy repo. It once ran as ONE
|
|
@@ -1093,7 +1170,7 @@ function isSpawnTimeout(result) {
|
|
|
1093
1170
|
}
|
|
1094
1171
|
|
|
1095
1172
|
function runGcloud(args, opts = {}) {
|
|
1096
|
-
return
|
|
1173
|
+
return spawnGroupBoundedSync("gcloud", args, {
|
|
1097
1174
|
stdio: opts.stdio || "inherit",
|
|
1098
1175
|
input: opts.input,
|
|
1099
1176
|
encoding: opts.encoding || "utf8",
|
|
@@ -1128,6 +1205,66 @@ function formatGcloudFailure(result) {
|
|
|
1128
1205
|
return "(no output captured from gcloud)";
|
|
1129
1206
|
}
|
|
1130
1207
|
|
|
1208
|
+
/** The thrown message for a failed `instances create` / `instances start`.
|
|
1209
|
+
*
|
|
1210
|
+
* Splits the two outcomes the old bare `Failed to create instance <name>`
|
|
1211
|
+
* collapsed, because they are different operator actions: a LOCAL timeout means
|
|
1212
|
+
* GCE may still be working the operation and it should be checked, while a
|
|
1213
|
+
* remote rejection means GCE said no and its own stderr is the answer. Both
|
|
1214
|
+
* arms put a cause in the transcript — classifyAddFailure keys on transcript
|
|
1215
|
+
* text, so a causeless message is what lands an add on the optimistic
|
|
1216
|
+
* `verifying` fallback. Pure: result in, string out.
|
|
1217
|
+
*
|
|
1218
|
+
* `verb` is the operator-facing word ("create" / "start"), not a wrapper verb. */
|
|
1219
|
+
/** Operator-facing copy for a create GCE accepted and then abandoned, kept
|
|
1220
|
+
* separate from the guard that detects it for the same reason
|
|
1221
|
+
* provisionFailureMessage is: the decision of WHEN to refuse and the wording of
|
|
1222
|
+
* WHAT to say are different concerns, and only the wording needs asserting.
|
|
1223
|
+
* Extracting it is what lets the copy be tested without a live gcloud — and on
|
|
1224
|
+
* this feature the copy IS the fix, since the bug was a message that told the
|
|
1225
|
+
* operator to wait for a VM that would never arrive.
|
|
1226
|
+
*
|
|
1227
|
+
* Every field is rendered defensively because this runs on a degraded path: the
|
|
1228
|
+
* operations read that produced `pending` is best-effort, so a partial record
|
|
1229
|
+
* must still yield a message that names the problem rather than printing
|
|
1230
|
+
* "undefined" at the operator. */
|
|
1231
|
+
function wedgedOperationMessage(instanceName, pending = {}) {
|
|
1232
|
+
const detail = [
|
|
1233
|
+
`${pending.name || "unnamed"}: ${pending.operationType || "unknown type"}`,
|
|
1234
|
+
`status ${pending.status || "unknown"}`,
|
|
1235
|
+
`progress ${pending.progress == null ? "unknown" : `${pending.progress}%`}`,
|
|
1236
|
+
`error ${pending.errorCode || "unknown"}${pending.errorMessage ? `: ${pending.errorMessage}` : ""}`,
|
|
1237
|
+
].join(", ");
|
|
1238
|
+
return (
|
|
1239
|
+
`Instance ${instanceName} is stuck in PROVISIONING behind a wedged create operation (${detail}). ` +
|
|
1240
|
+
"GCE accepted the create and never finished it, so this slot cannot be re-added and a delete will queue " +
|
|
1241
|
+
"behind the wedged operation rather than clear it. This needs GCE-side resolution — open a Google Cloud " +
|
|
1242
|
+
"Support case citing that error. Use a different VM number if you need a box now."
|
|
1243
|
+
);
|
|
1244
|
+
}
|
|
1245
|
+
|
|
1246
|
+
function provisionFailureMessage(verb, instanceName, result) {
|
|
1247
|
+
if (isSpawnTimeout(result)) {
|
|
1248
|
+
const minutes = Math.round(GCLOUD_PROVISION_TIMEOUT_MS / 60000);
|
|
1249
|
+
// A SIGTERMd child legitimately often captured nothing, and appending
|
|
1250
|
+
// formatGcloudFailure's "(no output captured)" placeholder to a message that
|
|
1251
|
+
// already states the cause is noise. Append it only when it is real output.
|
|
1252
|
+
const detail = formatGcloudFailure(result);
|
|
1253
|
+
const captured = detail === "(no output captured from gcloud)" ? "" : `\n${detail}`;
|
|
1254
|
+
return (
|
|
1255
|
+
`Failed to ${verb} instance ${instanceName}: the gcloud call exceeded the local ` +
|
|
1256
|
+
`${minutes}-minute provisioning budget and was SIGTERMd locally. GCE may still be ` +
|
|
1257
|
+
`working the operation — check it with \`gcloud compute operations list\` before ` +
|
|
1258
|
+
`retrying, and delete the half-built instance if one exists.` +
|
|
1259
|
+
captured
|
|
1260
|
+
);
|
|
1261
|
+
}
|
|
1262
|
+
return (
|
|
1263
|
+
`Failed to ${verb} instance ${instanceName} (gcloud exit ${result && result.status}).\n` +
|
|
1264
|
+
formatGcloudFailure(result)
|
|
1265
|
+
);
|
|
1266
|
+
}
|
|
1267
|
+
|
|
1131
1268
|
// A bounded, pure classifier that keys on status === 255 (gcloud-ssh's exit
|
|
1132
1269
|
// for an ssh failure) and a signature match against known transient strings.
|
|
1133
1270
|
function isTransientSshFailure(result) {
|
|
@@ -1203,6 +1340,38 @@ function fetchInstanceState(cfg) {
|
|
|
1203
1340
|
}
|
|
1204
1341
|
}
|
|
1205
1342
|
|
|
1343
|
+
/** Read the instance's outstanding (not-DONE) zone operations and summarize the
|
|
1344
|
+
* first one, or null when there are none / the call failed. Shaped as
|
|
1345
|
+
* { name, operationType, status, progress, errorCode, errorMessage } — the fields
|
|
1346
|
+
* the wedged-insert refusal message and the fleet classifier both need.
|
|
1347
|
+
*
|
|
1348
|
+
* Returning null on a failed or unparseable call is deliberate: this is a
|
|
1349
|
+
* DIAGNOSTIC read used to make a refusal more informative, so it must never turn
|
|
1350
|
+
* a working provision into a failing one just because the operations API hiccuped. */
|
|
1351
|
+
function fetchPendingOperation(cfg) {
|
|
1352
|
+
const result = runGcloud(buildGcloudArgs("operations-list-pending", cfg), {
|
|
1353
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
1354
|
+
});
|
|
1355
|
+
if (result.status !== 0) return null;
|
|
1356
|
+
let ops;
|
|
1357
|
+
try {
|
|
1358
|
+
ops = JSON.parse(result.stdout);
|
|
1359
|
+
} catch {
|
|
1360
|
+
return null;
|
|
1361
|
+
}
|
|
1362
|
+
if (!Array.isArray(ops) || ops.length === 0) return null;
|
|
1363
|
+
const op = ops[0];
|
|
1364
|
+
const err = op && op.error && Array.isArray(op.error.errors) ? op.error.errors[0] : null;
|
|
1365
|
+
return {
|
|
1366
|
+
name: op.name || null,
|
|
1367
|
+
operationType: op.operationType || null,
|
|
1368
|
+
status: op.status || null,
|
|
1369
|
+
progress: typeof op.progress === "number" ? op.progress : null,
|
|
1370
|
+
errorCode: (err && err.code) || null,
|
|
1371
|
+
errorMessage: (err && err.message) || null,
|
|
1372
|
+
};
|
|
1373
|
+
}
|
|
1374
|
+
|
|
1206
1375
|
function loadOrInitCloudConfig(projectDir, flags) {
|
|
1207
1376
|
const existing = loadCloudConfig(projectDir);
|
|
1208
1377
|
// noTunnel is a per-invocation behavior, never persisted — else a later manual
|
|
@@ -1284,7 +1453,11 @@ async function ensureFirewallRule(cfg) {
|
|
|
1284
1453
|
}
|
|
1285
1454
|
}
|
|
1286
1455
|
|
|
1287
|
-
|
|
1456
|
+
// `sleepFn` is injectable so the 60-iteration wait loop can be driven with a
|
|
1457
|
+
// no-op instead of burning 2 real minutes of clock — the same seam
|
|
1458
|
+
// runGcloudSshWithRetry / waitForEditorComposeRunning already expose.
|
|
1459
|
+
async function ensureInstance(cfg, { sleepFn } = {}) {
|
|
1460
|
+
const sleep = sleepFn || ((ms) => new Promise((r) => setTimeout(r, ms)));
|
|
1288
1461
|
console.error(`Checking instance ${cfg.instanceName}...`);
|
|
1289
1462
|
const initialState = fetchInstanceState(cfg);
|
|
1290
1463
|
const createdThisRun = initialState === null;
|
|
@@ -1295,29 +1468,59 @@ async function ensureInstance(cfg) {
|
|
|
1295
1468
|
}
|
|
1296
1469
|
if (initialState === "TERMINATED") {
|
|
1297
1470
|
console.error(` ${cfg.instanceName} is TERMINATED — starting...`);
|
|
1298
|
-
const start = runGcloud(buildGcloudArgs("instances-start", cfg)
|
|
1471
|
+
const start = runGcloud(buildGcloudArgs("instances-start", cfg), {
|
|
1472
|
+
timeout: gcloudCallTimeoutMs("instances-start"),
|
|
1473
|
+
});
|
|
1299
1474
|
if (start.status !== 0) {
|
|
1300
|
-
throw new Error(
|
|
1475
|
+
throw new Error(provisionFailureMessage("start", cfg.instanceName, start));
|
|
1301
1476
|
}
|
|
1302
1477
|
} else if (initialState === null) {
|
|
1303
1478
|
console.error(` ${cfg.instanceName} not found — creating...`);
|
|
1304
|
-
const create = runGcloud(buildGcloudArgs("instances-create", cfg)
|
|
1479
|
+
const create = runGcloud(buildGcloudArgs("instances-create", cfg), {
|
|
1480
|
+
timeout: gcloudCallTimeoutMs("instances-create"),
|
|
1481
|
+
});
|
|
1305
1482
|
if (create.status !== 0) {
|
|
1306
|
-
throw new Error(
|
|
1483
|
+
throw new Error(provisionFailureMessage("create", cfg.instanceName, create));
|
|
1307
1484
|
}
|
|
1308
1485
|
} else {
|
|
1486
|
+
// A slot found in PROVISIONING is ambiguous, and the two readings need
|
|
1487
|
+
// opposite handling. On a normal add it is a transient the poll loop below
|
|
1488
|
+
// resolves in seconds. But GCE can also accept an insert and then never
|
|
1489
|
+
// finish it (2026-09-04, VM-8): the operation parks at RUNNING/0%, eventually
|
|
1490
|
+
// attaches an INTERNAL_ERROR without ever reaching DONE, and holds the
|
|
1491
|
+
// instance name indefinitely — a delete queues behind it rather than clearing
|
|
1492
|
+
// it. Polling that for 2 minutes only rediscovers, slowly and with a message
|
|
1493
|
+
// naming neither the operation nor the wedge, a state the first describe
|
|
1494
|
+
// already showed. Read the outstanding operation instead and fail immediately
|
|
1495
|
+
// when it carries a terminal error, so the transcript carries the signature
|
|
1496
|
+
// fleet-add-failure.js's wedged-insert arm keys on.
|
|
1497
|
+
if (initialState === "PROVISIONING") {
|
|
1498
|
+
const pending = fetchPendingOperation(cfg);
|
|
1499
|
+
if (pending && pending.errorCode) {
|
|
1500
|
+
throw new Error(wedgedOperationMessage(cfg.instanceName, pending));
|
|
1501
|
+
}
|
|
1502
|
+
}
|
|
1309
1503
|
console.error(` ${cfg.instanceName} is ${initialState}; waiting...`);
|
|
1310
1504
|
}
|
|
1311
1505
|
|
|
1506
|
+
let lastState = initialState;
|
|
1312
1507
|
for (let i = 0; i < 60; i++) {
|
|
1313
|
-
|
|
1508
|
+
lastState = fetchInstanceState(cfg);
|
|
1509
|
+
if (lastState === "RUNNING") {
|
|
1314
1510
|
console.error(` ✓ ${cfg.instanceName} is RUNNING`);
|
|
1315
1511
|
await waitForIapReady(cfg);
|
|
1316
1512
|
return { createdThisRun };
|
|
1317
1513
|
}
|
|
1318
|
-
await
|
|
1514
|
+
await sleep(2000);
|
|
1319
1515
|
}
|
|
1320
|
-
|
|
1516
|
+
// Name the state it was actually stuck in. An instance parked in PROVISIONING
|
|
1517
|
+
// is a different operator action from one that stopped answering describe at
|
|
1518
|
+
// all, and the old message ("did not reach RUNNING") distinguished neither.
|
|
1519
|
+
throw new Error(
|
|
1520
|
+
`Instance ${cfg.instanceName} did not reach RUNNING within 2 minutes ` +
|
|
1521
|
+
`(last observed state: ${lastState || "unknown — describe returned no state"}` +
|
|
1522
|
+
`${createdThisRun ? "; a create was issued this run" : ""}).`,
|
|
1523
|
+
);
|
|
1321
1524
|
}
|
|
1322
1525
|
|
|
1323
1526
|
/** GCE reports the instance as RUNNING the moment the boot disk is attached,
|
|
@@ -1785,17 +1988,32 @@ function projectBootstrapRunnerScript(repoName) {
|
|
|
1785
1988
|
// makes the outcome probe honest: after a launch, a present `.done` can only
|
|
1786
1989
|
// mean THIS run's script reached its terminal line, never a leftover from a
|
|
1787
1990
|
// previous half-wired attempt.
|
|
1788
|
-
function projectBootstrapLaunchCommand(scriptBody, scriptArgs = [], envVars = {}, repoName) {
|
|
1991
|
+
function projectBootstrapLaunchCommand(scriptBody, scriptArgs = [], envVars = {}, repoName, libFiles = {}) {
|
|
1789
1992
|
const scriptB64 = Buffer.from(scriptBody, "utf8").toString("base64");
|
|
1790
1993
|
const runnerB64 = Buffer.from(projectBootstrapRunnerScript(repoName), "utf8").toString("base64");
|
|
1791
1994
|
const argsString = scriptArgs.map(shQuote).join(" ");
|
|
1792
1995
|
const envPrefix = envAssignmentPrefix(envVars);
|
|
1996
|
+
// The bootstrap script is written STANDALONE to $HOME, so any `source`/`.` it
|
|
1997
|
+
// does relative to its own directory resolves against $HOME — not against the
|
|
1998
|
+
// repo layout it was authored in. `libFiles` carries those siblings, keyed by
|
|
1999
|
+
// the $HOME-relative path the script will look for them at (e.g.
|
|
2000
|
+
// "lib/npm-pin.sh"). Their directories do not exist on a fresh VM, hence the
|
|
2001
|
+
// mkdir. Omitting a sourced library is not a soft failure: the script aborts
|
|
2002
|
+
// under `set -euo pipefail` and cloud:up tears the new VM down, which is the
|
|
2003
|
+
// 2026-09-04 fleet-wide add outage.
|
|
2004
|
+
const libWrites = Object.entries(libFiles).flatMap(([relPath, body]) => {
|
|
2005
|
+
const b64 = Buffer.from(body, "utf8").toString("base64");
|
|
2006
|
+
const dir = path.posix.dirname(relPath);
|
|
2007
|
+
const mkdir = dir && dir !== "." ? [`mkdir -p $HOME/${dir}`] : [];
|
|
2008
|
+
return [...mkdir, `echo '${b64}' | base64 -d > $HOME/${relPath}`];
|
|
2009
|
+
});
|
|
1793
2010
|
// Writes run synchronously (fast, before any backgrounding) so a decode error
|
|
1794
2011
|
// aborts before launch; then the runner is detached. The env prefix is on the
|
|
1795
2012
|
// setsid command so the tokens are inherited through nohup→setsid→bash but
|
|
1796
2013
|
// never appear in any argv. `& echo MARKER` (no `;` before echo) keeps the
|
|
1797
2014
|
// launch in the background while still confirming it fired.
|
|
1798
2015
|
const writes = [
|
|
2016
|
+
...libWrites,
|
|
1799
2017
|
`echo '${scriptB64}' | base64 -d > ${projectBootstrapScriptPath(repoName)}`,
|
|
1800
2018
|
`echo '${runnerB64}' | base64 -d > ${projectBootstrapRunnerPath(repoName)}`,
|
|
1801
2019
|
`rm -f ${projectBootstrapStatusPath(repoName)} ${projectBootstrapDoneMarker(repoName)}`,
|
|
@@ -2027,6 +2245,13 @@ async function ensureProjectBootstrapped(cfg, { sleepFn } = {}) {
|
|
|
2027
2245
|
console.error(` streaming scripts/gcp-project-bootstrap.sh to VM for ${cfg.repo}...`);
|
|
2028
2246
|
const scriptPath = path.join(rootDir, "scripts", "gcp-project-bootstrap.sh");
|
|
2029
2247
|
const scriptBody = readStreamedScript(scriptPath, cfg);
|
|
2248
|
+
// gcp-project-bootstrap.sh sources lib/npm-pin.sh relative to its own
|
|
2249
|
+
// directory. Read it through readStreamedScript too, so the library is subject
|
|
2250
|
+
// to the same drift guard as the script that sources it — streaming a reviewed
|
|
2251
|
+
// remote script beside a stale on-disk library would defeat the guard.
|
|
2252
|
+
const bootstrapLibs = {
|
|
2253
|
+
"lib/npm-pin.sh": readStreamedScript(path.join(rootDir, "scripts", "lib", "npm-pin.sh"), cfg),
|
|
2254
|
+
};
|
|
2030
2255
|
const scriptArgs = clientProjectBootstrapArgs(cfg);
|
|
2031
2256
|
// Git identity, forwarded launch-form keys, fleet-domain ingress and the admin
|
|
2032
2257
|
// toggle — see projectBootstrapEnv for what each key is for and when it is
|
|
@@ -2054,7 +2279,7 @@ async function ensureProjectBootstrapped(cfg, { sleepFn } = {}) {
|
|
|
2054
2279
|
// install that will survive an SSH disconnect. Every step of
|
|
2055
2280
|
// gcp-project-bootstrap.sh is skip-on-exists, so re-entering is cheap.
|
|
2056
2281
|
console.error(" launching detached project bootstrap on the VM (survives SSH disconnect)...");
|
|
2057
|
-
const launch = projectBootstrapLaunchCommand(scriptBody, scriptArgs, bootstrapEnv, repoName);
|
|
2282
|
+
const launch = projectBootstrapLaunchCommand(scriptBody, scriptArgs, bootstrapEnv, repoName, bootstrapLibs);
|
|
2058
2283
|
const launched = await runGcloudSshWithRetry(
|
|
2059
2284
|
"ssh-script",
|
|
2060
2285
|
cfg,
|
|
@@ -2805,8 +3030,30 @@ function buildEditorLocalSeedWriteCommand({ repoName, remotePath, b64 }) {
|
|
|
2805
3030
|
* `_managedPreviewOrigin` sentinel). We stamp the sentinel here too so the
|
|
2806
3031
|
* monitor doesn't mistake the cloud:up seed for a user pin. The
|
|
2807
3032
|
* skip-on-already-set guard stays: a non-empty previewOrigin (whether a user
|
|
2808
|
-
* pin or a monitor write) is left untouched on every `cloud:up`.
|
|
2809
|
-
|
|
3033
|
+
* pin or a monitor write) is left untouched on every `cloud:up`.
|
|
3034
|
+
*
|
|
3035
|
+
* `effectiveSameOriginSafe` is the RUNNING editor's own verdict
|
|
3036
|
+
* (`/api/config`'s `previewSameOriginSafe`), supplied by a caller that has
|
|
3037
|
+
* already fetched it. When it is a boolean it WINS over the static
|
|
3038
|
+
* `stack.json` read; `null`/absent leaves the static read in charge.
|
|
3039
|
+
*
|
|
3040
|
+
* That parameter exists because this function had two sources of truth for one
|
|
3041
|
+
* flag, and they disagreed exactly where the adaptive default lives. A
|
|
3042
|
+
* subpath hydration failure escalates a project off `/__codeyam_preview` at
|
|
3043
|
+
* RUNTIME even when `stack.json` never declared `sameOriginSafe: false` — which
|
|
3044
|
+
* is the documented Next.js scaffold shape (a static `false` broke every
|
|
3045
|
+
* SSH-forward VM, so the key is deliberately absent). `healPreviewOriginAtReady`
|
|
3046
|
+
* decided WHETHER to heal from that runtime verdict and was right, then called
|
|
3047
|
+
* this function, which recomputed WHAT to write from the static flag and wrote
|
|
3048
|
+
* the very same-origin sentinel that had just failed. The project then sat
|
|
3049
|
+
* permanently at `previewDedicatedOriginUnavailable: true` — the editor knowing
|
|
3050
|
+
* it must leave the subpath and having nowhere to go — beside a preview host
|
|
3051
|
+
* that was already provisioned, LB-routed, HEALTHY and unused. */
|
|
3052
|
+
async function seedPreviewOrigin(
|
|
3053
|
+
cfg,
|
|
3054
|
+
appPort = VITE_PORT,
|
|
3055
|
+
effectiveSameOriginSafe = null,
|
|
3056
|
+
) {
|
|
2810
3057
|
const repoName = resolveProject(cfg).name;
|
|
2811
3058
|
const remotePath = `~/projects/${repoName}/.codeyam/editor.local.json`;
|
|
2812
3059
|
const stackPath = `~/projects/${repoName}/.codeyam/stack.json`;
|
|
@@ -2842,6 +3089,12 @@ async function seedPreviewOrigin(cfg, appPort = VITE_PORT) {
|
|
|
2842
3089
|
current = parseObj(localJson);
|
|
2843
3090
|
sameOriginSafe = previewSameOriginSafeFromStack(parseObj(stackJson));
|
|
2844
3091
|
}
|
|
3092
|
+
// The running editor's verdict overrides the static file — see
|
|
3093
|
+
// `resolveEffectiveSameOriginSafe` for why only a boolean counts.
|
|
3094
|
+
sameOriginSafe = resolveEffectiveSameOriginSafe(
|
|
3095
|
+
sameOriginSafe,
|
|
3096
|
+
effectiveSameOriginSafe,
|
|
3097
|
+
);
|
|
2845
3098
|
|
|
2846
3099
|
// Resolve this VM's custom slug (if any) so a dedicated-origin seed uses the
|
|
2847
3100
|
// slug host. The slug rides on cfg at Add time (the --vm-slug flag, validated
|
|
@@ -2875,6 +3128,12 @@ async function seedPreviewOrigin(cfg, appPort = VITE_PORT) {
|
|
|
2875
3128
|
// `sameOriginSafe: true`, and seeded `/__codeyam_preview` — which a
|
|
2876
3129
|
// remote browser then loads as `127.0.0.1 refused to connect`). Once
|
|
2877
3130
|
// stack.json exists, heal it forward to the dedicated origin.
|
|
3131
|
+
// `sameOriginSafe` here is the EFFECTIVE verdict, so this exception also
|
|
3132
|
+
// covers the case stack.json can never report: a project whose static
|
|
3133
|
+
// flag is legitimately absent (the Next.js scaffold default) that
|
|
3134
|
+
// escalated off the subpath at runtime after a hydration failure. The
|
|
3135
|
+
// caller passes the editor's own answer; without it this predicate read
|
|
3136
|
+
// `true` and declined to heal the one shape it was needed for.
|
|
2878
3137
|
// Because every dashboard action (Reset / Update / Add / bootstrap-recovery)
|
|
2879
3138
|
// re-runs this seed over cloud:up, either heal reaches every touch-point the
|
|
2880
3139
|
// moment stack.json is present — closing the race without re-ordering
|
|
@@ -2970,11 +3229,14 @@ async function healPreviewOriginAtReady(cfg, appPort = VITE_PORT) {
|
|
|
2970
3229
|
`READY auto-heal: ${cfg.instanceName} reports a not-same-origin-safe stack ` +
|
|
2971
3230
|
`but a wrong-mode previewOrigin (${config.previewOrigin}); re-seeding...`,
|
|
2972
3231
|
);
|
|
2973
|
-
// Re-run the seed
|
|
2974
|
-
//
|
|
2975
|
-
//
|
|
2976
|
-
//
|
|
2977
|
-
|
|
3232
|
+
// Re-run the seed with the verdict we just read, so WHETHER to heal and WHAT
|
|
3233
|
+
// to write come from the same source. Passing it is what makes this heal reach
|
|
3234
|
+
// the adaptive-default stacks it exists for: on a Next.js project whose
|
|
3235
|
+
// `stack.json` omits `sameOriginSafe` (the scaffolded shape), the static read
|
|
3236
|
+
// inside the seed answers `true` and re-writes the same-origin sentinel that
|
|
3237
|
+
// just failed to hydrate. We got here precisely BECAUSE the editor said
|
|
3238
|
+
// `false`, so `false` is the answer the seed must use.
|
|
3239
|
+
await seedPreviewOrigin(cfg, appPort, false);
|
|
2978
3240
|
// Tell the running server to re-read editor.local.json — no restart (the
|
|
2979
3241
|
// footgun that killed VM7's live Claude session).
|
|
2980
3242
|
const reload = runGcloud(
|
|
@@ -3372,7 +3634,16 @@ function promptForInstanceName(expected) {
|
|
|
3372
3634
|
});
|
|
3373
3635
|
}
|
|
3374
3636
|
|
|
3375
|
-
|
|
3637
|
+
// Every file streamed to a VM, and therefore every file the drift guard must
|
|
3638
|
+
// compare against the reviewed remote tip. `scripts/lib/npm-pin.sh` is here
|
|
3639
|
+
// because gcp-project-bootstrap.sh SOURCES it: a library that reaches the VM is
|
|
3640
|
+
// as much streamed content as the script that sources it, and leaving it out
|
|
3641
|
+
// meant the drift guard silently vouched for only half of what was shipped.
|
|
3642
|
+
const STREAMED_SCRIPTS = [
|
|
3643
|
+
"scripts/gcp-bootstrap.sh",
|
|
3644
|
+
"scripts/gcp-project-bootstrap.sh",
|
|
3645
|
+
"scripts/lib/npm-pin.sh",
|
|
3646
|
+
];
|
|
3376
3647
|
|
|
3377
3648
|
/** The configured editor branch to compare/stream streamed scripts against.
|
|
3378
3649
|
* Resolved from config (`resolveEditorConfig` → `editorBranch`, itself derived
|
|
@@ -4202,6 +4473,75 @@ function releaseVmSlugOnDestroy(cfg) {
|
|
|
4202
4473
|
return vmNumber;
|
|
4203
4474
|
}
|
|
4204
4475
|
|
|
4476
|
+
/** Does `branch` still exist on `repo`? Answered with a single
|
|
4477
|
+
* `git ls-remote --heads` — about a second, and no cloud resource touched.
|
|
4478
|
+
*
|
|
4479
|
+
* Tri-state on purpose, and the third state is the load-bearing one:
|
|
4480
|
+
* true — the ref resolved.
|
|
4481
|
+
* false — the remote answered and the ref is NOT there. Provably gone.
|
|
4482
|
+
* null — we could not get an answer (no network, no credentials, git
|
|
4483
|
+
* missing, timeout). NOT evidence of a deleted branch.
|
|
4484
|
+
* Only `false` refuses an add. Collapsing `null` into `false` would turn every
|
|
4485
|
+
* transient GitHub blip into a fleet-wide add outage, which is a far worse
|
|
4486
|
+
* failure than the one this preflight exists to prevent — and the classifier
|
|
4487
|
+
* arm downstream still catches a genuinely dead ref, just later and dearer.
|
|
4488
|
+
*
|
|
4489
|
+
* A private repo needs credentials, so the token is inlined into the URL the
|
|
4490
|
+
* same way clone_editor_repo does it on the VM side (x-access-token), and for
|
|
4491
|
+
* the same reason: the plain URL 401s. The token is confined to this argv and
|
|
4492
|
+
* never logged — the caller prints `repo` (the un-inlined form) instead. */
|
|
4493
|
+
function gitBranchRefExists({ repo, branch }) {
|
|
4494
|
+
if (!repo || !branch) return null;
|
|
4495
|
+
let url = repo;
|
|
4496
|
+
const token = resolveGithubToken();
|
|
4497
|
+
if (token && /^https:\/\/github\.com\//.test(repo)) {
|
|
4498
|
+
url = repo.replace("https://", `https://x-access-token:${token}@`);
|
|
4499
|
+
}
|
|
4500
|
+
const res = spawnSync("git", ["ls-remote", "--heads", url, branch], {
|
|
4501
|
+
encoding: "utf8",
|
|
4502
|
+
timeout: 30000,
|
|
4503
|
+
});
|
|
4504
|
+
// A non-zero exit is an unreachable/unauthorized remote, not an absent branch:
|
|
4505
|
+
// git exits 0 and prints nothing for "connected fine, no such head".
|
|
4506
|
+
if (res.error || res.status !== 0) return null;
|
|
4507
|
+
return String(res.stdout || "").trim().length > 0;
|
|
4508
|
+
}
|
|
4509
|
+
|
|
4510
|
+
/** Refuse an add whose stored branch has been deleted from origin, BEFORE the
|
|
4511
|
+
* first gcloud call.
|
|
4512
|
+
*
|
|
4513
|
+
* Without this the failure lands as late and as expensively as it possibly can:
|
|
4514
|
+
* on 2026-08-25 four fleet VMs each created a real GCE instance, booted it,
|
|
4515
|
+
* installed npm and cloned 13,226 files before `git fetch` inside the VM said
|
|
4516
|
+
* `fatal: couldn't find remote ref editor-improvements-75` — a branch pruned
|
|
4517
|
+
* from origin hours earlier when it merged. One `git ls-remote` establishes the
|
|
4518
|
+
* same fact in about a second, for free. This repo prunes branches on merge, so
|
|
4519
|
+
* ANY stored feature-branch name expires silently; that makes this a recurring
|
|
4520
|
+
* condition, not a one-off.
|
|
4521
|
+
*
|
|
4522
|
+
* Placed beside checkScriptDrift and assertVariantProvisionable — the other
|
|
4523
|
+
* "fail loud before creating a GCE instance" guards — because it is only worth
|
|
4524
|
+
* anything ahead of ensureInstance. It cannot live in scripts/gcp-bootstrap.sh,
|
|
4525
|
+
* which already runs ON the provisioned box and never sees the client branch.
|
|
4526
|
+
*
|
|
4527
|
+
* Throws with the field, the branch and the repo named. Never throws for a ref
|
|
4528
|
+
* it could not check (see gitBranchRefExists). */
|
|
4529
|
+
function assertBranchRefsExist(cfg, { refExists = gitBranchRefExists } = {}) {
|
|
4530
|
+
// Lazy require: launch-config.js requires THIS module at load time, so a
|
|
4531
|
+
// top-level require here would close the cycle. Same pattern as the
|
|
4532
|
+
// isPlaceholderProject call sites above.
|
|
4533
|
+
const lc = require("./launch-config");
|
|
4534
|
+
const { editorRepo, editorBranch } = resolveEditorConfig(cfg);
|
|
4535
|
+
const targets = lc.branchRefTargets(
|
|
4536
|
+
{ clientRepo: cfg.repo, clientBranch: cfg.branch, editorBranch },
|
|
4537
|
+
{ editorRepo },
|
|
4538
|
+
);
|
|
4539
|
+
if (!targets.length) return;
|
|
4540
|
+
console.error("Checking that the requested branches still exist on origin...");
|
|
4541
|
+
const missing = lc.missingBranchRefs(targets, refExists);
|
|
4542
|
+
if (missing.length) throw new Error(lc.missingBranchRefMessage(missing));
|
|
4543
|
+
}
|
|
4544
|
+
|
|
4205
4545
|
async function cmdUp(args, projectDir = rootDir) {
|
|
4206
4546
|
ensureGcloud();
|
|
4207
4547
|
const flags = parseFlags(args);
|
|
@@ -4256,6 +4596,10 @@ async function cmdUp(args, projectDir = rootDir) {
|
|
|
4256
4596
|
|
|
4257
4597
|
checkScriptDrift(cfg);
|
|
4258
4598
|
|
|
4599
|
+
// Last of the pre-instance guards: a deleted branch ref is checkable in ~1s
|
|
4600
|
+
// and otherwise costs a full instance + boot + 13k-file clone to discover.
|
|
4601
|
+
assertBranchRefsExist(cfg);
|
|
4602
|
+
|
|
4259
4603
|
await ensureFirewallRule(cfg);
|
|
4260
4604
|
const { createdThisRun } = await ensureInstance(cfg);
|
|
4261
4605
|
|
|
@@ -4631,6 +4975,10 @@ async function cmdIngressUp(args, projectDir = rootDir) {
|
|
|
4631
4975
|
async function cmdReconcileIngress(args, projectDir = rootDir) {
|
|
4632
4976
|
ensureGcloud();
|
|
4633
4977
|
parseFlags(args);
|
|
4978
|
+
// Clear ssh sessions earlier timed-out probes leaked before this sweep can add
|
|
4979
|
+
// load of its own. Only PID-1 orphans with the fleet signature — never a live
|
|
4980
|
+
// sweep's in-flight children.
|
|
4981
|
+
reapOrphanedFleetSsh();
|
|
4634
4982
|
const cfg = loadCloudConfig(projectDir) || {};
|
|
4635
4983
|
const domain = resolveIngressDomain(cfg, process.env);
|
|
4636
4984
|
if (!domain) {
|
|
@@ -4661,7 +5009,7 @@ async function cmdReconcileIngress(args, projectDir = rootDir) {
|
|
|
4661
5009
|
// whole sweep. On timeout res.stdout is empty, so parseIngressStatusVerdict
|
|
4662
5010
|
// returns null → an `unknown` verdict → selectIngressReconcileTargets SKIPS
|
|
4663
5011
|
// this VM and the sweep continues to the next one.
|
|
4664
|
-
const res =
|
|
5012
|
+
const res = spawnGroupBoundedSync(inv.command, inv.args, {
|
|
4665
5013
|
env: { ...process.env, ...inv.env },
|
|
4666
5014
|
encoding: "utf8",
|
|
4667
5015
|
timeout: GCLOUD_CALL_TIMEOUT_MS,
|
|
@@ -4722,7 +5070,7 @@ async function cmdReconcileIngress(args, projectDir = rootDir) {
|
|
|
4722
5070
|
// Bound the per-BES converge too: a hung `gcloud backend-services update`
|
|
4723
5071
|
// returns a non-zero (timed-out) result, so it falls into the skip branch
|
|
4724
5072
|
// below and the sweep moves on to the next BES rather than stalling.
|
|
4725
|
-
const res =
|
|
5073
|
+
const res = spawnGroupBoundedSync(inv.command, inv.args, {
|
|
4726
5074
|
encoding: "utf8",
|
|
4727
5075
|
timeout: GCLOUD_CALL_TIMEOUT_MS,
|
|
4728
5076
|
});
|
|
@@ -5299,6 +5647,9 @@ async function cloudMain() {
|
|
|
5299
5647
|
module.exports = {
|
|
5300
5648
|
// pure helpers
|
|
5301
5649
|
isSpawnTimeout,
|
|
5650
|
+
gcloudCallTimeoutMs,
|
|
5651
|
+
provisionFailureMessage,
|
|
5652
|
+
GCLOUD_PROVISION_TIMEOUT_MS,
|
|
5302
5653
|
agentEnvVarName,
|
|
5303
5654
|
agentNpmPackage,
|
|
5304
5655
|
AGENT_ENV_VARS,
|
|
@@ -5334,6 +5685,7 @@ module.exports = {
|
|
|
5334
5685
|
resolveConfiguredAppPort,
|
|
5335
5686
|
resolveSeedPreviewOrigin,
|
|
5336
5687
|
previewSameOriginSafeFromStack,
|
|
5688
|
+
resolveEffectiveSameOriginSafe,
|
|
5337
5689
|
isRottedManagedPreviewOrigin,
|
|
5338
5690
|
isWrongModePreviewOrigin,
|
|
5339
5691
|
isSilentlyBrokenPreview,
|
|
@@ -5392,6 +5744,8 @@ module.exports = {
|
|
|
5392
5744
|
FORWARD_ENV_KEYS_ENV,
|
|
5393
5745
|
parseEditorComposeState,
|
|
5394
5746
|
fetchInstanceState,
|
|
5747
|
+
fetchPendingOperation,
|
|
5748
|
+
wedgedOperationMessage,
|
|
5395
5749
|
formatGcloudFailure,
|
|
5396
5750
|
isTransientSshFailure,
|
|
5397
5751
|
runGcloudSshWithRetry,
|
|
@@ -5449,6 +5803,8 @@ module.exports = {
|
|
|
5449
5803
|
readVmAppPort,
|
|
5450
5804
|
buildVmAppPortReadCommand,
|
|
5451
5805
|
resolveLaptopGitIdentity,
|
|
5806
|
+
gitBranchRefExists,
|
|
5807
|
+
assertBranchRefsExist,
|
|
5452
5808
|
};
|
|
5453
5809
|
|
|
5454
5810
|
if (require.main === module) {
|
package/npm/editor-dev.js
CHANGED
|
@@ -40,6 +40,8 @@ const {
|
|
|
40
40
|
logLauncherTeardownSignal,
|
|
41
41
|
readMergedEditorConfig,
|
|
42
42
|
resolveLauncherPort,
|
|
43
|
+
readServerOutputLogTail,
|
|
44
|
+
formatLaunchFailureError,
|
|
43
45
|
staleBinaryBanner,
|
|
44
46
|
enforceFreshBinary,
|
|
45
47
|
} = require("./utils");
|
|
@@ -447,9 +449,29 @@ if (require.main === module) {
|
|
|
447
449
|
buildLauncherArtifacts();
|
|
448
450
|
}
|
|
449
451
|
|
|
452
|
+
// Both helpers take the port to BIND plus the TARGET PROJECT dir — the
|
|
453
|
+
// latter is what lets them find the project's `server-state.json`, and so
|
|
454
|
+
// find a running server whose control port is not `SERVER_PORT` at all.
|
|
450
455
|
const serverChild = shouldRestart
|
|
451
|
-
? await restartServer(SERVER_PORT)
|
|
452
|
-
: await ensureServer(SERVER_PORT);
|
|
456
|
+
? await restartServer(SERVER_PORT, projectDir)
|
|
457
|
+
: await ensureServer(SERVER_PORT, projectDir);
|
|
458
|
+
|
|
459
|
+
// Nothing below may claim the editor is up until something is actually
|
|
460
|
+
// listening on the port we just bound. `ensureServer` returns null only
|
|
461
|
+
// when it ATTACHED to a live server, and its spawn path already races the
|
|
462
|
+
// port — but a launcher that prints a URL it never bound turned a
|
|
463
|
+
// recoverable port conflict into a debugging session, so the banner is
|
|
464
|
+
// gated on an independent probe rather than on that internal contract.
|
|
465
|
+
if (!(await waitForPort(SERVER_PORT, 10000, 200))) {
|
|
466
|
+
console.error(
|
|
467
|
+
formatLaunchFailureError(
|
|
468
|
+
SERVER_PORT,
|
|
469
|
+
projectDir,
|
|
470
|
+
readServerOutputLogTail(projectDir),
|
|
471
|
+
),
|
|
472
|
+
);
|
|
473
|
+
process.exit(1);
|
|
474
|
+
}
|
|
453
475
|
|
|
454
476
|
// Re-read the (possibly just-initialized) project config to decide
|
|
455
477
|
// whether to wait on a dev server at all. On a project where init
|